' opencode.bmx

Strict

Import "host.bmx"

Type TDiff
	Field	pos,count,del$,add$,pos0,count0,pos1
End Type

Function WordAtPos$(a$,p)
	Local	c,q,r,n
' string literal
	For q=0 To p-1
		If a$[q]=34 n=Not n;r=q
	Next
	If n	
		q=a.Find(Chr(34),r+1)+1
		If q=0 q=a.length
		Return a[r..q]
	EndIf
' alphanumeric
	For p=p To 0 Step -1
		c=a$[p]
		If c<48 Continue
		If c>=58 And c<65 Continue
		If c>=91 And c<97 Continue
		If c>=123 Continue
		Exit
	Next
	For q=p-1 To 0 Step -1
		c=a$[q]
		If c<48 Exit
		If c>=58 And c<65 Exit
		If c>=91 And c<97 Exit
		If c>=123 Exit
	Next
	For r=p To a.length-1
		c=a$[r]
		If c<48 Exit
		If c>=58 And c<65 Exit
		If c>=91 And c<97 Exit
		If c>=123 Exit
	Next
	Return a$[q+1..r]
End Function

Function FirstDiff(s0$,s1$)
	Local	n,i
	n=Min(s0.length,s1.length)
	For i=0 Until n
		If s0[i]<>s1[i] Return i
	Next
	Return n		
End Function

Function LastDiff(s0$,s1$)
	Local	n,i,j
	n=Min(s0.length,s1.length)
	i=s0.length-1
	j=s1.length-1
	While n>0
		If s0[i]<>s1[j] Exit
		i:-1;j:-1;n:-1
	Wend
	Return i+1
End Function

Function GlobalX(gadget:TGadget)
	Local x=4
	While gadget
		x:+GadgetX(gadget)
		gadget=GadgetGroup(gadget)
	Wend
	Return x
End Function

Function GlobalY(gadget:TGadget)
	Local y=80
	While gadget
		y:+GadgetY(gadget)
		gadget=GadgetGroup(gadget)
	Wend
	Return y
End Function

Type TCodeNode Extends TDataNode
	Field	owner:TOpenCode
	Field	pos,count

	Method Invoke(command$,argument:Object=Null)
		Select command
			Case "ACTIVATE"
				owner.ShowPos(pos)
		End Select
	End Method

	Method Sync(snap:TDataNode)
		Local	n:TCodeNode		
		If snap.name<>name SetName(snap.name)
		n=TCodeNode(snap)
		If n pos=n.pos;count=n.count
		Super.Sync(snap)
	End Method
	
	Method SetName(n$)
		Local	p
		p=n.find("'")
		If p<>-1 n=n[..p]
		name=n.Trim()
	End Method
		
	Method AddCodeNode:TCodeNode(n$,p0,p1)
		Local	c:TCodeNode
'		print "AddCodeNode "+n$
		c=New TCodeNode
		c.owner=owner
		c.setname n$
		c.pos=p0
		c.count=p1-p0
		Append c
		Return c
	End Method
End Type

Type TOpenFile Extends TToolPanel
	Field	_filetime
	Method snapshot()
		_filetime=FileTime(path)
	End Method
End Type

Type TOpenCode Extends TOpenFile

	Field	host:THost

	Field	textarea:TGadget
	Field	dirty=True
	Field	filesrc$,cleansrc$,cleansrcl$
	Field	current:TDiff
	Field	undolist:TList=New TList
	Field	redolist:TList=New TList
	Field	helpcmd$,helpstring$
	Field	seek$
	Field	cursorpos,cursorlen,cursorline
	Field	oldpos,oldlen
	Field	deferpos
	Field	codenode:TDataNode
	Field	dirtynode,uc
	Field	root:TCodeNode
	Field	autoindentenabled=True
	
	Method Help()
	End Method

	Method Refresh() Abstract
	Method SetCode(src$,diff:TDiff=Null) Abstract

' prepends "." to file name with code borrowed from SaveAs		

	Method MakePathTemp()
		Local file$=ExtractDir(path)+"/."+StripDir(path)
		SaveSource(file$)
		GetNode().Refresh
		host.SetTitle path
	End Method

	Method BuildSource(quick,debug,gui,run)		
		Local ex$=ExtractExt(path)
		If ex.ToLower()="bat"
			Local cd$=CurrentDir$()
			ChangeDir ExtractDir(path)
			Local cmd$=StripDir(path)
			host.execute Null,cmd,"Running "+cmd,"",True
			ChangeDir cd
		EndIf
	End Method
	
	Method GetNode:TDataNode()
		Local	src$,line,col
		Local	p,p1,r,t,m,f,l,e
		If Not root
			root=New TCodeNode
			root.name=StripDir(path)
			root.owner=Self
		EndIf
		Return root
	End Method

	Method CheckDirty(src$)
		Local	bool
		bool=True
		If src=filesrc And undolist.isempty() bool=False
		SetDirty bool
	End Method

	Method ShowPos(pos)
		host.SelectPanel Self
		SelectTextAreaText textarea,TextAreaLine(textarea,pos),0,TEXTAREA_LINES
		SelectTextAreaText textarea,TextAreaLine(textarea,pos),1,TEXTAREA_LINES
		UpdateCursor
		ActivateGadget textarea
	End Method

	Method Debug(line,column)
		SelectTextAreaText textarea,line-1,0,TEXTAREA_LINES
		SelectTextAreaText textarea,line-1,1,TEXTAREA_LINES
		ActivateGadget textarea
		UpdateStatus
	End Method

	Method Edit()
		SelectTextAreaText textarea,cursorpos,0,TEXTAREA_CHARS
		ActivateGadget textarea
		UpdateStatus
	End Method
	
	Method UpdateStatus()
		Local	c
		c=cursorpos+cursorlen
		If cursorline c=c-TextAreaChar(textarea,cursorline-1)
		host.SetStatus helpstring+"~t~t"+"Line: "+(cursorline)+" Char: "+(c+1)+"       "
	End Method
	
	Method UpdateCursor()
		Local	l	
		oldpos=cursorpos
		oldlen=cursorlen
		cursorpos=TextAreaCursor(textarea,TEXTAREA_CHARS)
		cursorlen=TextAreaSelLen(textarea,TEXTAREA_CHARS)
		If cursorpos<>oldpos Or cursorlen<>oldlen
			l=TextAreaLine(textarea,cursorpos)+1
			If l<>cursorline And dirtynode
				GetNode().Refresh
				dirtynode=False
				If deferpos SetCode cleansrc,Null
			EndIf
			cursorline=l
			UpdateStatus()
		EndIf
	End Method

' tdiff - pos del$ add$

	Method CalcDiff:TDiff(src$)
		Local	d:TDiff

		If src.length<>cleansrc.length
			d=New TDiff
			d.pos0=cursorpos
			d.count0=cursorlen
			d.pos=oldpos
			d.count=oldlen
			If cursorlen And oldlen							'block modified
				d.del=cleansrc[oldpos..oldpos+oldlen]
				d.add=src[oldpos..cursorpos+cursorlen]
				d.pos1=oldpos
			Else
				If cursorpos<=oldpos And cursorlen<=oldlen	'backspace
					d.del=cleansrc[cursorpos..cursorpos+cleansrc.length-src.length]
					d.pos1=cursorpos
				Else										'insert
					d.del=cleansrc[oldpos..oldpos+oldlen]
					d.add=src[oldpos..cursorpos+cursorlen]
					d.pos1=oldpos
				EndIf
			EndIf
			Return d		
		EndIf		
		If cursorpos>oldpos									'overwrite
			d=New TDiff
			d.pos0=cursorpos
			d.count0=cursorlen
			d.pos=oldpos
			d.count=oldlen
			d.del=cleansrc[oldpos..cursorpos]
			d.add=src[oldpos..cursorpos]
			d.pos1=oldpos
			Return d		
		EndIf		
	End Method

	Method UpdateCode(makeundo=True)
		Local	cpos,src$
		Local	d:TDiff

		src=TextAreaText(textarea)		
		d=CalcDiff(src)
		If d		
'			print "d.pos="+d.pos+" d.count="+d.count+" d.add=*"+d.add+"* d.del=*"+d.del+"*"
			If makeundo
				undolist.AddLast d
				redolist.Clear
			EndIf
			SetCode src,d
			dirtynode=True
		Else
			If deferpos SetCode src,Null
		EndIf
	End Method
	
	Method Undo()
		Local	d:TDiff
		If undolist.isempty() Return
		d=TDiff(undolist.RemoveLast())
		redolist.AddLast d
		SetTextAreaText textarea,d.del,d.pos1,d.add.length
		SelectTextAreaText(textarea,d.pos,d.count)
		SetCode TextAreaText(textarea),d
		UpdateCursor
	End Method

	Method Redo()
		Local	d:TDiff
		If redolist.isempty() Return
		d=TDiff(redolist.RemoveLast())
		undolist.AddLast d
		SetTextAreaText textarea,d.add,d.pos,d.del.length
		SelectTextAreaText(textarea,d.pos0,d.count0)
		UpdateCursor
		SetCode TextAreaText(textarea),d
	End Method

	Function IsntAlphaNumeric(c)		'lowercase test only
		If c<47 Return True
		If c>=58 And c<95 Return True
		If c=96 Return True
		If c>=123 Return True
	End Function
	
	Function IsntAlphaNumericOrQuote(c)		'lowercase test only
		If c=34 Return False
		If c<47 Return True
		If c>=58 And c<95 Return True
		If c=96 Return True
		If c>=123 Return True
	End Function

	Method AutoIndent()
		Local	t$,p,q,c,n
		c=TextAreaCursor(textarea,TEXTAREA_CHARS)
		n=TextAreaSelLen(textarea,TEXTAREA_CHARS)
		If c 
			p=cleansrc.FindLast(EOL$,cleansrc.length-(c-1))+1
			q=p
			While cleansrc[q]=9
				q:+1
			Wend
			If q>c q=c
		EndIf
		SetTextAreaText textarea,EOL$+cleansrc[p..q],c,n
		SelectTextAreaText textarea,c+1+q-p,0			
		UpdateCursor
		UpdateCode
	End Method

	Method IndentCode()
		Local	cpos,p0,p1,i,a$
		If TextAreaSelLen( textarea,TEXTAREA_CHARS )
' blockindent
			p0=TextAreaCursor(textarea,TEXTAREA_LINES)
			p1=TextAreaSelLen(textarea,TEXTAREA_LINES)
			For i=0 Until p1
				a$="~t"+TextAreaText(textarea,p0+i,1,TEXTAREA_LINES)
				SetTextAreaText textarea,a$,p0+i,1,TEXTAREA_LINES
			Next
			SelectTextAreaText textarea,p0,p1,TEXTAREA_LINES
		Else
' insert normal tab
			cpos=TextAreaCursor(textarea,TEXTAREA_CHARS)
			SetTextAreaText textarea,"~t",cpos,0		
			SelectTextAreaText textarea,cpos+1,0
		EndIf
		UpdateCursor
		UpdateCode
	End Method

	Method OutdentCode()
		Local	cpos,p0,p1,i,a$,modified
' blockoutdent
		p0=TextAreaCursor(textarea,TEXTAREA_LINES)
		p1=TextAreaSelLen(textarea,TEXTAREA_LINES)
		For i=0 Until p1
			a$=TextAreaText(textarea,p0+i,1,TEXTAREA_LINES)
			If a[0]=9 a$=a$[1..];modified=True
			SetTextAreaText textarea,a$,p0+i,1,TEXTAREA_LINES
		Next
		If Not modified
			For i=0 Until p1
				a$=TextAreaText(textarea,p0+i,1,TEXTAREA_LINES)
				If a[0]=32 a$=a$[1..]
				SetTextAreaText textarea,a$,p0+i,1,TEXTAREA_LINES
			Next	
		EndIf
		SelectTextAreaText textarea,p0,p1,TEXTAREA_LINES
		UpdateCursor
		UpdateCode
	End Method

	
	Function FilterKey(event:TEvent,context:Object)
		Local	this:TOpenCode
		Local	id,key,mods
'		If event.id<>EVENT_KEYCHAR Return 1
		id=event.id
		key=event.data
		mods=event.mods
		this=TOpenCode(context)
?MacOS
		If key=25 And mods=1 key=9
?
		If id=EVENT_KEYCHAR And this And key=9 And TextAreaSelLen( this.textarea,TEXTAREA_CHARS )
			Select mods
				Case 0
					this.IndentCode
				Case 1
					this.OutdentCode
			End Select
			Return 0
		EndIf

		If id=EVENT_KEYDOWN And this And key=13 'And autoindentenabled'this.host.options.autoindent 
			this.AutoIndent 
			Return 0
		EndIf
		
		Return 1
	End Function

	Method SetDirty( bool )
		If dirty=bool Return
		dirty=bool
		name=StripDir(path)
		If (dirty) name:+"*"
		If (host.lockedpanel=Self) name="build:"+name
		host.RefreshPanel Self
	End Method
	
	Method SetLocked( bool )
		Local locked:TOpenCode
		locked=TOpenCode(host.lockedpanel)
		If locked And locked<>Self locked.SetLocked False
		name=StripDir(path)
		If (dirty) name:+"*"
		If (bool) 
			name="build:"+name
			host.lockedpanel=Self
		Else
			host.lockedpanel=Null			
		EndIf
		host.RefreshPanel Self
	End Method
	
	Method Find()
		Local	p,l$,a$
		p=TextAreaCursor(textarea,TEXTAREA_CHARS)
		a$=WordAtPos(cleansrc,p)		
		host.ShowFind a$
	End Method
		
	Method FindNext(s$)
		Local	p,l$
		If s seek=s Else s=seek
		p=TextAreaCursor(textarea,TEXTAREA_CHARS)
		p:+TextAreaSelLen(textarea,TEXTAREA_CHARS)
' case insensitive
		l$=s.toLower()
		p=cleansrcl.Find(l$,p)
		If p=-1 p=cleansrcl.Find(l$)
' case sensitive
'		p=cleansrc.Find(s$,p+1)
'		if p=-1 p=cleansrc.Find(s$)
		If p=-1
			Notify "Could not find "+s$
			Return False
		Else
			SelectTextAreaText textarea,p,Len s,TEXTAREA_CHARS
			UpdateCursor
			Return True
		EndIf
	End Method

	Method ReplaceAll(s$,r$)
		Local	p,c,l$,src$
		c=TextAreaCursor(textarea,TEXTAREA_CHARS)
		s$=s.toLower()
		While True
			src=TextAreaText(textarea)
			l=src.ToLower()
			p=l.Find(s$,p)
			If p=-1 Exit
			SelectTextAreaText textarea,p,s.length
			UpdateCursor
			UpdateCode		
			SetTextAreaText textarea,r,p,s.length
			If p<c c=c+r.length-s.length			
			p=p+r.length
			SelectTextAreaText textarea,p,0
			UpdateCursor
			UpdateCode
		Wend
		SelectTextAreaText textarea,c,0
		UpdateCursor
	End Method
	
	Method FindReplace(r$)
		Local	p,n,f$,x$
		p=r.Find(Chr(0))
		If p>0
			f$=r[..p]
			r$=r[p+1..]
			ReplaceAll f$,r$
		Else
			p=TextAreaCursor(textarea,TEXTAREA_CHARS)
			n=TextAreaSelLen(textarea,TEXTAREA_CHARS)
			If n=0 Return
			SetTextAreaText textarea,r$,p,n
			SelectTextAreaText textarea,p+Len r,0
			UpdateCursor
			UpdateCode		
		EndIf
		Return True
	End Method
	
	Method ReadSource(path$)
		Local	src$		
		src=LoadText(path)
		src=src.Replace(Chr(13),"")
		src=src.Replace(Chr(11),"")
		If Not src src=Chr(10)
		LockTextArea textarea
		SetTextAreaText textarea,src
		UnlockTextArea textarea
		filesrc=TextAreaText(textarea)
		cleansrc=""
		cleansrcl=""
		ActivateGadget textarea		
	End Method

	Method SaveSource(file$)
		Local	src$		
		src=TextAreaText(textarea)
		filesrc=src
		src=src.Replace$(Chr(13),Chr(10))
		src=src.Replace$(Chr(11),"")
		Local txt$=src.Replace$(Chr(10),Chr(13)+Chr(10))		
		host.Save file,txt
		path=host.FullPath$(file)
		dirty=True
		SetDirty False
		host.AddRecent(path$)
		Return True
	End Method

	Method Save()
		Local	file$
		file=path
		If path.find("/tmp/")<>-1		'host.IsTempPath(path) 
			file=RequestFile("Save As","",True,"")
			If file="" Return False
			If ExtractExt(file)="" file=file+".bmx"
			dirty=True
		EndIf
		If dirty SaveSource(file)
		Return True
	End Method
' common command interface

	Method Invoke(command$,argument:Object=Null)
		Local	file$,ex$
		Local	p,res
		Select command

			Case "ACTIVATE"
				If textarea ActivateGadget textarea
				GetNode().Refresh		

			Case "SHOW"
				If textarea ActivateGadget textarea
				host.SetCodeNode GetNode()
				host.SetTitle path				
				UpdateStatus()
			Case "CLOSE"
				If dirty
					p=Proceed("Save changes to "+name)	'the current file?
					If p=-1 Return True
					If p=1 
						If Not Save() Return True
					EndIf
				EndIf
				If codenode codenode.detach;codenode=Null
				host.RemovePanel Self
			Case "SAVE"
				Save
			Case "QUICKSAVE"
				file=path
				If dirty SaveSource(file)
			Case "SAVEAS"
				file=path
				If host.IsTempPath(path) file$=StripDir(file)
				file=RequestFile("Save As",host.filefilters(ExtractExt(file)),True,file)
				If file="" Return
				ex$=ExtractExt(file)
				If ex$="" file=file+".bmx"
				SaveSource(file$)
				If codenode codenode.detach;codenode=Null
				host.RemovePanel Self
				host.OpenSource file
			Case "GOTO"
				Local line=Int(String(argument))
				SelectTextAreaText textarea,line-1,0,TEXTAREA_LINES				
				UpdateCursor
				ActivateGadget textarea		
			Case "FIND"
				Find
			Case "FINDNEXT"
				res=FindNext(String(argument))
				ActivateGadget textarea
				Return res		
			Case "REPLACE"
				Return FindReplace(String(argument))	
			Case "BUILD"
				BuildSource host.quickenabled,host.debugenabled,host.guienabled,False
			Case "RUN"
				BuildSource host.quickenabled,host.debugenabled,host.guienabled,True
			Case "LOCK"
				SetLocked True
			Case "UNLOCK"
				SetLocked False			
			Case "HELP"
				Help
			Case "UNDO"
				Undo
			Case "REDO"
				Redo
			Case "REFRESH"
				Refresh
			Case "CUT"
				GadgetCut textarea
			Case "COPY"
				GadgetCopy textarea	
			Case "PASTE"
				GadgetPaste textarea
			Case "SELECTALL"
				SelectTextAreaText textarea
			Case "INDENT"
				IndentCode
			Case "OUTDENT"
				OutdentCode
			Case "PRINT"
				GadgetPrint textarea				
		End Select
	End Method
End Type
